-- -------------------------------------------------------------------------
-- UI/widgets.lua
-- Widget System
-- -------------------------------------------------------------------------

local widgets = {}

local state = require("ClickUl.UI.state")
local renderer = require("ClickUl.UI.renderer")
local input_module = require("ClickUl.UI.input")
local layout = require("ClickUl.UI.layout")
local utf8_utils = require("ClickUl.Lib.utf8_utils")

-- [Lua 5.4] <const> for global type references
local color <const> = color

-- [Lua 5.4] <const> for function references
local math_floor <const> = math.floor
local math_max <const> = math.max
local math_min <const> = math.min
local math_abs <const> = math.abs
local string_len <const> = string.len
local string_sub <const> = string.sub
local string_format <const> = string.format
local os_clock <const> = os.clock
local table_insert <const> = table.insert
local table_concat <const> = table.concat
local ipairs <const> = ipairs
local tostring <const> = tostring
local type <const> = type

-- [Renderer Function References]
local get_clean_label <const> = renderer.get_clean_label
local get_text_width <const> = renderer.get_text_width
local draw_rect <const> = renderer.draw_rect
local draw_rect_ex <const> = renderer.draw_rect_ex
local draw_text <const> = renderer.draw_text
local draw_text_ex <const> = renderer.draw_text_ex
local draw_widget_background <const> = renderer.draw_widget_background
local draw_slider_background <const> = renderer.draw_slider_background
local get_visual_y <const> = renderer.get_visual_y
local is_item_visible <const> = renderer.is_item_visible
local s_func <const> = renderer.s
local is_mouse_in <const> = input_module.is_mouse_in

local slider_value_cache = {}

local cp_hex_cache = {}

local progress_pct_cache = {}
for i = 0, 100 do
    progress_pct_cache[i] = tostring(i) .. "%"
end

-- [Helpers]

local function trigger_click_anim(x, y, w, h)
    table_insert(state.ui.click_animations, {
        x = x, y = y, w = w, h = h,
        start_time = os_clock(),
        duration = state.constants.CLICK_ANIMATION_DURATION
    })
end

-- Checkbox animation: stores animation state per checkbox ID
local function get_checkbox_anim(id)
    local anims = state.ui.checkbox_animations
    if not anims[id] then
        anims[id] = { progress = 0, target = 0, last_update = 0 }
    end
    return anims[id]
end

local function update_checkbox_anim(id, is_checked)
    local anim = get_checkbox_anim(id)
    local target = is_checked and 1 or 0
    if anim.target ~= target then
        anim.target = target
        anim.last_update = os_clock()
    end
    -- Smooth animation (lerp)
    local speed = 8
    anim.last_update = os_clock()
    local diff = anim.target - anim.progress
    if math_abs(diff) > 0.001 then
        anim.progress = anim.progress + diff * math_min(1, speed * 0.016)
    else
        anim.progress = anim.target
    end
    return anim.progress
end

-- [Combo Dropdown Renderer]

local function draw_combo_dropdown(args)
    local ui = state.ui
    local c_state = ui.combo_states[args.id]
    if not c_state then return end

    local x, y, w, h = args.x, args.y, args.w, args.h
    local options = args.options
    local count = #options
    local vis_count = math_min(count, args.max_visible)
    local list_h = vis_count * h
    local ly = y + h + args.gap

    -- Draw Background
    draw_rect_ex(x, ly, w, list_h, "Combo Background", args.rounding)

    -- Scroll Logic
    local start_idx = c_state.scroll_idx
    if count > args.max_visible then
        local max_scroll = count - vis_count

        -- Mouse Wheel
        if is_mouse_in(x, ly, w, list_h) then
             local N_DIS = state.natives_cache.IS_DISABLED_CONTROL_JUST_PRESSED
             if N_DIS and N_DIS(0, 241) then c_state.scroll_idx = math_max(1, c_state.scroll_idx - 1); ui.scroll_consumed = true
             elseif N_DIS and N_DIS(0, 242) then c_state.scroll_idx = math_min(max_scroll + 1, c_state.scroll_idx + 1); ui.scroll_consumed = true end
        end

        -- Scrollbar
        local sb_w = args.sb_width
        local bar_x = x + w - sb_w - s_func(2)
        local bar_h = list_h - args.gap
        local sb_id = args.id .. "_sb"

        local is_hover = is_mouse_in(bar_x - 5, ly, sb_w + 10, bar_h)
        if ui.input.mouse_left_pressed and is_hover and not ui.active_id then
            ui.active_id = sb_id
        end

        if ui.active_id == sb_id then
            if not ui.input.mouse_left_pressed then ui.active_id = nil
            else
                local r = (ui.input.mouse_pos.y - ly) / bar_h
                c_state.scroll_idx = math_floor(r * max_scroll) + 1
            end
        end

        -- Clamp
        c_state.scroll_idx = math_max(1, math_min(c_state.scroll_idx, max_scroll + 1))
        start_idx = c_state.scroll_idx

        -- Draw Thumb
        local ratio = vis_count / count
        local th = math_max(args.thumb_min, bar_h * ratio)
        local ty_offset = ((bar_h - th) * ((start_idx - 1) / max_scroll))
        local sb_col = (ui.active_id == sb_id) and "Scrollbar Hover" or "Scrollbar"
        draw_rect_ex(bar_x, ly + ty_offset, sb_w, th, sb_col, s_func(2))
    end

    -- Draw Items
    local end_idx = math_min(count, start_idx + vis_count - 1)
    for i = start_idx, end_idx do
        local rel_i = i - start_idx
        local iy = ly + (rel_i * h)

        if is_mouse_in(x, iy, w, h) then
            draw_rect_ex(x, iy, w, h, "Combo Highlight", args.gap)
            if ui.input.mouse_left_just_pressed and not is_mouse_in(x+w-20, ly, 20, list_h) then -- Avoid scrollbar click
                c_state.pending_selection = i
                ui.active_combo = nil
            end
        end

        local col = (i == args.current_index) and "Accent" or "Text Primary"
        draw_text_ex(options[i], x + args.padding, iy, args.font, col, nil, h)
    end

    -- Close on click outside
    if ui.input.mouse_left_just_pressed and not is_mouse_in(x, ly, w, list_h) and not is_mouse_in(x, y, w, h) then
        ui.active_combo = nil
    end
end

-- [Widgets]

function widgets.Button(label, hint)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local ui_input = ui.input
    local w, h = layout.GetWidth(), cfg.button_h or cfg.item_h
    local vy = get_visual_y()
    local clicked = false

    if is_item_visible(vy, h) then
        local hover = is_mouse_in(ui.cx, vy, w, h)
        if ui.active_combo or ui.active_id == "window_scrollbar" then hover = false end

        -- Update menu hint on hover
        if hover and hint then
            ui.menu_hint.current = hint
            ui.menu_hint.last_update = os_clock()
        end

        if hover and ui_input.mouse_left_just_pressed then
            clicked = true
            trigger_click_anim(ui.cx, vy, w, h)
        end

        draw_widget_background(ui.cx, vy, w, h, hover)
        draw_text(get_clean_label(label), ui.cx, vy, cfg.font_widget, hover and "Text Primary" or "Text Secondary", w, h)
    end
    ui.cy = ui.cy + h + cfg.item_spacing
    return clicked
end

function widgets.Checkbox(label, value, hint)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local ui_input = ui.input
    local w, h = layout.GetWidth(), cfg.item_h
    local vy = get_visual_y()

    if is_item_visible(vy, h) then
        local hover = is_mouse_in(ui.cx, vy, w, h)
        if ui.active_combo then hover = false end

        -- Update menu hint on hover
        if hover and hint then
            ui.menu_hint.current = hint
            ui.menu_hint.last_update = os_clock()
        end

        if hover and ui_input.mouse_left_just_pressed then
            value = not value
            trigger_click_anim(ui.cx, vy, w, h)
        end

        draw_widget_background(ui.cx, vy, w, h, hover)

        -- Generate unique animation ID using context stack
        local anim_id = state.get_widget_id(label)

        -- Choose style based on configuration
        if cfg.checkbox_use_toggle_style then
            -- Toggle switch style: left text + right switch (with animation)
            draw_text(get_clean_label(label), ui.cx + cfg.widget_text_padding, vy, cfg.font_widget, (value or hover) and "Text Primary" or "Text Secondary", nil, h)

            local cw = cfg.checkbox_width or 40
            local ch = cfg.checkbox_height or 22
            local tx = ui.cx + w - cw - cfg.widget_text_padding
            local ty = vy + (h - ch) * 0.5

            -- Get animation progress (0 = off, 1 = on)
            local anim_progress = update_checkbox_anim(anim_id, value)

            -- Draw track with animated color blend (use Toggle colors for consistency)
            local track_rounding = cfg.toggle_track_rounding or cfg.checkbox_track_rounding or 11
            if anim_progress < 0.01 then
                draw_rect(tx, ty, cw, ch, "Toggle Inactive", track_rounding)
            elseif anim_progress > 0.99 then
                draw_rect(tx, ty, cw, ch, "Toggle Active", track_rounding)
            else
                draw_rect(tx, ty, cw, ch, "Toggle Inactive", track_rounding)
                local active_col = state.deps.config_sys.data.colors["Toggle Active"]
                if active_col then
                    local blended = color(active_col.r, active_col.g, active_col.b, math_floor(active_col.a * anim_progress))
                    draw_rect(tx, ty, cw, ch, blended, track_rounding)
                end
            end

            -- Draw knob with animated position and color (use Toggle settings for consistency)
            local ks = cfg.toggle_knob_size or cfg.checkbox_knob_size or 18
            local ko = cfg.toggle_knob_offset or cfg.checkbox_knob_offset or 2
            local kx_off = tx + ko
            local kx_on = tx + cw - ks - ko
            local kx = kx_off + (kx_on - kx_off) * anim_progress
            local knob_color = anim_progress > 0.5 and "Knob Active" or "Knob Inactive"
            draw_rect(kx, ty + (ch - ks) * 0.5, ks, ks, knob_color, cfg.toggle_knob_rounding or ks * 0.5)
        else
            -- Box style: left box + right text (with animation)
            local box_size = cfg.checkbox_height or 22
            local box_rounding = cfg.checkbox_knob_rounding or 4
            local box_x = ui.cx + cfg.widget_text_padding
            local box_y = vy + (h - box_size) * 0.5

            -- Get animation progress (0 = unchecked, 1 = checked)
            local anim_progress = update_checkbox_anim(anim_id, value)

            -- Draw outer box (inactive color as background)
            draw_rect(box_x, box_y, box_size, box_size, "Checkbox Inactive", box_rounding)

            -- Draw inner fill with scale animation
            if anim_progress > 0.01 then
                local inner_margin = 3
                local inner_size = (box_size - inner_margin * 2) * anim_progress
                local inner_offset = (box_size - inner_size) * 0.5
                draw_rect(
                    box_x + inner_offset,
                    box_y + inner_offset,
                    inner_size,
                    inner_size,
                    "Checkbox Active",
                    math_max(1, box_rounding * anim_progress)
                )
            end

            local text_x = box_x + box_size + cfg.widget_text_padding
            draw_text(get_clean_label(label), text_x, vy, cfg.font_widget, (value or hover) and "Text Primary" or "Text Secondary", nil, h)
        end
    end
    ui.cy = ui.cy + h + cfg.item_spacing
    return value
end

--- Toggle switch component (text on left, switch on right)
--- @param label string Label text
--- @param value boolean Current value
--- @return boolean New value
function widgets.Toggle(label, value, hint)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local ui_input = ui.input
    local w, h = layout.GetWidth(), cfg.item_h
    local vy = get_visual_y()

    if is_item_visible(vy, h) then
        local hover = is_mouse_in(ui.cx, vy, w, h)
        if ui.active_combo then hover = false end

        -- Update menu hint on hover
        if hover and hint then
            ui.menu_hint.current = hint
            ui.menu_hint.last_update = os_clock()
        end

        if hover and ui_input.mouse_left_just_pressed then
            value = not value
            trigger_click_anim(ui.cx, vy, w, h)
        end

        draw_widget_background(ui.cx, vy, w, h, hover)

        -- Left: text
        draw_text(get_clean_label(label), ui.cx + cfg.widget_text_padding, vy, cfg.font_widget, (value or hover) and "Text Primary" or "Text Secondary", nil, h)

        -- Right: toggle switch with animation
        local tw = cfg.toggle_width or cfg.checkbox_width or 40
        local th = cfg.toggle_height or cfg.checkbox_height or 22
        local tx = ui.cx + w - tw - cfg.widget_text_padding
        local ty = vy + (h - th) * 0.5

        -- Get animation progress (0 = off, 1 = on)
        local anim_progress = update_checkbox_anim("toggle_" .. label, value)

        -- Draw track with animated color blend
        local track_rounding = cfg.toggle_track_rounding or cfg.checkbox_track_rounding or 11
        if anim_progress < 0.01 then
            draw_rect(tx, ty, tw, th, "Toggle Inactive", track_rounding)
        elseif anim_progress > 0.99 then
            draw_rect(tx, ty, tw, th, "Toggle Active", track_rounding)
        else
            -- Draw both colors for blend effect
            draw_rect(tx, ty, tw, th, "Toggle Inactive", track_rounding)
            local active_col = state.deps.config_sys.data.colors["Toggle Active"]
            if active_col then
                local blended = color(active_col.r, active_col.g, active_col.b, math_floor(active_col.a * anim_progress))
                draw_rect(tx, ty, tw, th, blended, track_rounding)
            end
        end

        -- Draw knob with animated position and color
        local ks = cfg.toggle_knob_size or cfg.checkbox_knob_size or 18
        local ko = cfg.toggle_knob_offset or cfg.checkbox_knob_offset or 2
        local kx_off = tx + ko
        local kx_on = tx + tw - ks - ko
        local kx = kx_off + (kx_on - kx_off) * anim_progress
        local knob_color = anim_progress > 0.5 and "Knob Active" or "Knob Inactive"
        draw_rect(kx, ty + (th - ks) * 0.5, ks, ks, knob_color, cfg.toggle_knob_rounding or ks * 0.5)
    end
    ui.cy = ui.cy + h + cfg.item_spacing
    return value
end

function widgets.Slider(label, value, min, max, step, hint)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local ui_input = ui.input
    step = (type(step) == "number" and step > 0) and step or 1
    local w, h = layout.GetWidth(), cfg.slider_h or (cfg.item_h + (cfg.slider_extra_height or s_func(20)))
    local vy = get_visual_y()
    local id = label

    if is_item_visible(vy, h) or ui.active_id == id then
        local hover = is_mouse_in(ui.cx, vy, w, h)
        if ui.active_combo then hover = false end

        -- Update menu hint on hover
        if hover and hint then
            ui.menu_hint.current = hint
            ui.menu_hint.last_update = os_clock()
        end

        if hover and ui_input.mouse_left_just_pressed then ui.active_id = id end

        local padding = cfg.widget_text_padding
        local track_x = ui.cx + padding
        local track_w = w - (padding * 2)

        if ui.active_id == id then
            if not ui_input.mouse_left_pressed then ui.active_id = nil
            else
                local r = (ui_input.mouse_pos.x - track_x) / track_w
                value = min + (math_max(0, math_min(1, r)) * (max - min))
                if step > 0 then value = math_floor((value / step) + 0.5) * step end
            end
        end

        value = math_max(min, math_min(value, max))

        draw_slider_background(ui.cx, vy, w, h, (ui.active_id == id or hover))
        draw_text(get_clean_label(label), track_x, vy + s_func(4), cfg.font_small, "Text Secondary")

        local st = slider_value_cache[id]
        if not st then
            st = { last_val = nil, text_cache = "" }
            slider_value_cache[id] = st
        end

        local PRECISION = 1000000
        local rounded_val = math_floor(value * PRECISION + 0.5) / PRECISION

        local vs = st.text_cache
        if st.last_val ~= rounded_val then
            if step % 1 == 0 then
                vs = tostring(math_floor(value))
            else
                vs = string_format("%.2f", value)
            end
            st.text_cache = vs
            st.last_val = rounded_val
        end

        local val_w = get_text_width(vs, cfg.font_small)
        draw_text(vs, ui.cx + w - val_w - padding, vy + s_func(4), cfg.font_small, "Accent")

        local sy = vy + (cfg.slider_track_offset or s_func(30))
        local sh = cfg.slider_height or s_func(4)

        draw_rect(track_x, sy, track_w, sh, "Slider Empty", s_func(2))

        local ratio = (max ~= min) and ((value - min) / (max - min)) or 0
        local fill_w = math_max(cfg.widget_rounding, track_w * ratio)
        local col = (ui.active_id == id or hover) and "Slider Filled Hover" or "Slider Filled"
        draw_rect(track_x, sy, fill_w, sh, col, s_func(2))
    end
    ui.cy = ui.cy + h + cfg.item_spacing
    return value
end

function widgets.Combo(label, current_index, options, hint)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local ui_input = ui.input
    local w, h = layout.GetWidth(), cfg.item_h
    local vy = get_visual_y()

    -- Handle empty options
    if not options or #options == 0 then
        if is_item_visible(vy, h) then
            draw_widget_background(ui.cx, vy, w, h, false)
            draw_text("(Empty)", ui.cx + cfg.widget_text_padding, vy, cfg.font_widget, "Text Secondary", nil, h)
        end
        ui.cy = ui.cy + h + cfg.item_spacing
        return current_index or 1
    end

    current_index = current_index or 1

    -- Generate unique ID using context stack (handles nested Groups automatically)
    local id = state.get_widget_id(label)
    if not ui.combo_states[id] then
        ui.combo_states[id] = { scroll_idx = 1, dragging_sb = false }
    end
    local c_state = ui.combo_states[id]

    if c_state.pending_selection then
        current_index = c_state.pending_selection
        c_state.pending_selection = nil
    end

    if is_item_visible(vy, h) then
        local active = (ui.active_combo == id)
        local hover = is_mouse_in(ui.cx, vy, w, h)
        if ui.active_combo and not active then hover = false end

        -- Update menu hint on hover
        if hover and hint then
            ui.menu_hint.current = hint
            ui.menu_hint.last_update = os_clock()
        end

        draw_widget_background(ui.cx, vy, w, h, active or hover)
        draw_text(options[current_index] or "", ui.cx + cfg.widget_text_padding, vy, cfg.font_widget, "Text Primary", nil, h)

        local clean = get_clean_label(label)
        local lw = get_text_width(clean, cfg.font_small)
        draw_text(clean, ui.cx + w - lw - cfg.widget_text_padding, vy, cfg.font_small, "Text Secondary", nil, h)

        if active then
            if not c_state.render_payload then
                c_state.render_payload = {
                    func = draw_combo_dropdown,
                    args = {}
                }
            end

            local a = c_state.render_payload.args
            a.id = id; a.options = options; a.current_index = current_index
            a.x = ui.cx; a.y = vy; a.w = w; a.h = h
            a.max_visible = cfg.combo_max_visible or 5
            a.gap = cfg.combo_dropdown_gap; a.padding = cfg.widget_text_padding
            a.font = cfg.font_widget; a.rounding = cfg.widget_rounding
            a.thumb_min = cfg.combo_thumb_min; a.sb_width = cfg.scrollbar_width

            table_insert(ui.post_draw_queue, c_state.render_payload)
        elseif hover and ui_input.mouse_left_just_pressed then
            ui.active_combo = id
            c_state.scroll_idx = 1
            if current_index > (cfg.combo_max_visible or 5) then
                c_state.scroll_idx = math_max(1, current_index - 2)
            end
        end
    end
    ui.cy = ui.cy + h + cfg.item_spacing
    return current_index
end

function widgets.TextInput(label, text, max_len)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local w, h = layout.GetWidth(), cfg.item_h
    local vy = get_visual_y()

    -- Generate unique ID using context stack (handles nested Groups automatically)
    local id = state.get_widget_id(label)

    if not ui.input_states[id] then
        local initial = text or ""
        ui.input_states[id] = {
            text = initial,
            cursor_pos = utf8_utils.clamp_cursor_pos(initial, string_len(initial)),
            active = false,
            blink_time = 0,
            -- Key repeat state
            backspace_time = 0,
            delete_time = 0,
            left_time = 0,
            right_time = 0,
            -- Selection state
            sel_start = nil,  -- nil means no selection
            sel_end = nil,
            dragging = false
        }
    end
    local st = ui.input_states[id]

    -- Helper: Check if there's an active selection
    local function has_selection()
        return st.sel_start and st.sel_end and st.sel_start ~= st.sel_end
    end

    -- Helper: Get ordered selection bounds (min, max)
    local function get_selection_bounds()
        if not has_selection() then return nil, nil end
        local s, e = st.sel_start, st.sel_end
        if s > e then s, e = e, s end
        return s, e
    end

    -- Helper: Delete selected text and return true if deleted
    local function delete_selection()
        if not has_selection() then return false end
        local s, e = get_selection_bounds()
        st.text = string_sub(st.text, 1, s) .. string_sub(st.text, e + 1)
        st.cursor_pos = s
        st.sel_start, st.sel_end = nil, nil
        return true
    end

    -- Helper: Get selected text
    local function get_selected_text()
        if not has_selection() then return "" end
        local s, e = get_selection_bounds()
        return string_sub(st.text, s + 1, e)
    end

    local function calc_cursor_from_mouse(mouse_x, text_x)
        local rel_x = mouse_x - text_x
        if rel_x <= 0 then return 0 end

        local total_w = 0
        local prev_w = 0
        local pos = 0

        for char in st.text:gmatch("[%z\1-\127\194-\244][\128-\191]*") do
            local next_pos = pos + #char
            local char_w = get_text_width(char, cfg.font_widget)
            total_w = total_w + char_w

            if total_w > rel_x then
                if (rel_x - prev_w) < (total_w - rel_x) then
                    return pos
                else
                    return next_pos
                end
            end

            prev_w = total_w
            pos = next_pos
        end

        return string_len(st.text)
    end

    if is_item_visible(vy, h) then
        local hover = is_mouse_in(ui.cx, vy, w, h)
        if ui.active_combo then hover = false end
        local active = (ui.active_id == id)
        local tx = ui.cx + cfg.widget_text_padding

        -- Mouse click handling for activation and cursor positioning
        if hover and ui.input.mouse_left_just_pressed then
            if not active then
                ui.active_id = id; st.active = true; st.blink_time = os_clock(); ui.text_input_active = true
            end
            -- Position cursor at click location
            local click_pos = calc_cursor_from_mouse(ui.input.mouse_pos.x, tx)
            st.cursor_pos = click_pos
            st.sel_start = click_pos
            st.sel_end = click_pos
            st.dragging = true
        elseif active and ui.input.mouse_left_just_pressed and not hover then
            ui.active_id = nil; st.active = false; ui.text_input_active = false
            st.sel_start, st.sel_end = nil, nil
            st.dragging = false
        end

        -- Mouse drag for selection
        if active and st.dragging then
            if ui.input.mouse_left_pressed then
                local drag_pos = calc_cursor_from_mouse(ui.input.mouse_pos.x, tx)
                st.cursor_pos = drag_pos
                st.sel_end = drag_pos
            else
                st.dragging = false
            end
        end

        if active then
            st.cursor_pos = utf8_utils.clamp_cursor_pos(st.text, st.cursor_pos)
        end

        if active and input then
            local key_func = input.key
            st.cursor_pos = utf8_utils.clamp_cursor_pos(st.text, st.cursor_pos)
            local text_len_bytes = string_len(st.text)
            local now = os_clock()
            local max_chars = max_len or 64

            -- Key repeat config
            local repeat_delay = 0.4
            local repeat_rate = 0.05

            local function should_repeat(key_code, time_field)
                local key_state = key_func(key_code)
                if key_state.just_pressed then
                    st[time_field] = now
                    return true
                elseif key_state.pressed then
                    local elapsed = now - st[time_field]
                    if elapsed > repeat_delay then
                        local repeat_elapsed = elapsed - repeat_delay
                        local repeat_count = math_floor(repeat_elapsed / repeat_rate)
                        local last_repeat = st[time_field .. "_last"] or 0
                        if repeat_count > last_repeat then
                            st[time_field .. "_last"] = repeat_count
                            return true
                        end
                    end
                else
                    st[time_field] = 0
                    st[time_field .. "_last"] = 0
                end
                return false
            end

            local ctrl = key_func(0x11).pressed
            local shift = key_func(0x10).pressed or key_func(0xA0).pressed or key_func(0xA1).pressed
            local caps_state = key_func(0x14)
            if caps_state and caps_state.just_pressed then
                ui.caps_lock = not ui.caps_lock
            end
            local caps = ui.caps_lock

            -- Ctrl+C Copy
            if ctrl and key_func(0x43).just_pressed then
                if has_selection() and input.set_clipboard_text then
                    local sel_text = get_selected_text()
                    if sel_text ~= "" then
                        pcall(input.set_clipboard_text, sel_text)
                    end
                end
            -- Ctrl+X Cut
            elseif ctrl and key_func(0x58).just_pressed then
                if has_selection() and input.set_clipboard_text then
                    local sel_text = get_selected_text()
                    if sel_text ~= "" then
                        pcall(input.set_clipboard_text, sel_text)
                        delete_selection()
                    end
                end
            -- Ctrl+V Paste
            elseif ctrl and key_func(0x56).just_pressed then
                if input.get_clipboard_text then
                    local clipboard = input.get_clipboard_text()
                    if clipboard and clipboard ~= "" then
                        local PASTE_LIMIT = 2000
                        if #clipboard > PASTE_LIMIT then
                            clipboard = string_sub(clipboard, 1, PASTE_LIMIT)
                        end
                        clipboard = clipboard:gsub("[\r\n]", "")
                        delete_selection()
                        local cur_chars = utf8_utils.char_count(st.text)
                        local paste_chars = utf8_utils.char_count(clipboard)
                        local available = max_chars - cur_chars
                        if available > 0 then
                            if paste_chars > available then
                                local truncated_parts = {}
                                local count = 0
                                for char in clipboard:gmatch("[%z\1-\127\194-\244][\128-\191]*") do
                                    if count >= available then break end
                                    count = count + 1
                                    truncated_parts[count] = char
                                end
                                clipboard = table_concat(truncated_parts)
                            end
                            st.text = string_sub(st.text, 1, st.cursor_pos) .. clipboard .. string_sub(st.text, st.cursor_pos + 1)
                            st.cursor_pos = st.cursor_pos + #clipboard
                        end
                    end
                end
            -- Ctrl+A Select All
            elseif ctrl and key_func(0x41).just_pressed then
                st.sel_start = 0
                st.sel_end = text_len_bytes
                st.cursor_pos = text_len_bytes
            -- Backspace
            elseif should_repeat(0x08, "backspace_time") then
                if has_selection() then
                    delete_selection()
                elseif st.cursor_pos > 0 then
                    local prev_pos = utf8_utils.prev_char_pos(st.text, st.cursor_pos)
                    st.text = string_sub(st.text, 1, prev_pos) .. string_sub(st.text, st.cursor_pos + 1)
                    st.cursor_pos = prev_pos
                end
            -- Delete
            elseif should_repeat(0x2E, "delete_time") then
                if has_selection() then
                    delete_selection()
                elseif st.cursor_pos < text_len_bytes then
                    local next_pos = utf8_utils.next_char_pos(st.text, st.cursor_pos)
                    st.text = string_sub(st.text, 1, st.cursor_pos) .. string_sub(st.text, next_pos + 1)
                end
            -- Left arrow (with Shift for selection)
            elseif should_repeat(0x25, "left_time") and st.cursor_pos > 0 then
                local new_pos = utf8_utils.prev_char_pos(st.text, st.cursor_pos)
                if shift then
                    if not st.sel_start then st.sel_start = st.cursor_pos end
                    st.sel_end = new_pos
                else
                    st.sel_start, st.sel_end = nil, nil
                end
                st.cursor_pos = new_pos
            -- Right arrow (with Shift for selection)
            elseif should_repeat(0x27, "right_time") and st.cursor_pos < text_len_bytes then
                local new_pos = utf8_utils.next_char_pos(st.text, st.cursor_pos)
                if shift then
                    if not st.sel_start then st.sel_start = st.cursor_pos end
                    st.sel_end = new_pos
                else
                    st.sel_start, st.sel_end = nil, nil
                end
                st.cursor_pos = new_pos
            -- Home (with Shift for selection)
            elseif key_func(0x24).just_pressed then
                if shift then
                    if not st.sel_start then st.sel_start = st.cursor_pos end
                    st.sel_end = 0
                else
                    st.sel_start, st.sel_end = nil, nil
                end
                st.cursor_pos = 0
            -- End (with Shift for selection)
            elseif key_func(0x23).just_pressed then
                if shift then
                    if not st.sel_start then st.sel_start = st.cursor_pos end
                    st.sel_end = text_len_bytes
                else
                    st.sel_start, st.sel_end = nil, nil
                end
                st.cursor_pos = text_len_bytes
            -- Enter/Esc to exit
            elseif key_func(0x0D).just_pressed or key_func(0x1B).just_pressed then
                ui.active_id = nil; st.active = false; ui.text_input_active = false
                st.sel_start, st.sel_end = nil, nil
            -- Character input (allow if not using Ctrl shortcuts)
            elseif not (ctrl and (key_func(0x43).pressed or key_func(0x56).pressed or
                                   key_func(0x58).pressed or key_func(0x41).pressed)) then
                local vk_list = input_module.get_vk_list()
                local cur_chars = utf8_utils.char_count(st.text)

                for _, k in ipairs(vk_list) do
                    if key_func(k.code).just_pressed then
                        -- Delete selection first if any
                        if has_selection() then
                            delete_selection()
                            cur_chars = utf8_utils.char_count(st.text)
                        end
                        if cur_chars < max_chars then
                            local use_shift = shift
                            if k.code >= 0x41 and k.code <= 0x5A then
                                use_shift = (shift ~= caps)
                            end
                            local char = use_shift and k.shift or k.char
                            st.text = string_sub(st.text, 1, st.cursor_pos) .. char .. string_sub(st.text, st.cursor_pos + 1)
                            st.cursor_pos = st.cursor_pos + #char
                        end
                        break
                    end
                end
            end
        end

        draw_widget_background(ui.cx, vy, w, h, active or hover)

        local clean = get_clean_label(label)
        local lw = get_text_width(clean, cfg.font_small)
        draw_text(clean, ui.cx + w - lw - cfg.widget_text_padding, vy, cfg.font_small, "Text Secondary", nil, h)

        local disp = st.text
        if string_len(disp) == 0 and not active then
            draw_text("...", tx, vy, cfg.font_widget, "Text Secondary", nil, h)
        else
            -- Draw selection highlight
            if active and has_selection() then
                local sel_s, sel_e = get_selection_bounds()
                local sel_x1 = tx + get_text_width(string_sub(st.text, 1, sel_s), cfg.font_widget)
                local sel_x2 = tx + get_text_width(string_sub(st.text, 1, sel_e), cfg.font_widget)
                draw_rect(sel_x1, vy + s_func(4), sel_x2 - sel_x1, h - s_func(8), "Accent")
            end

            draw_text(disp, tx, vy, cfg.font_widget, "Text Primary", nil, h)

            -- Draw cursor (only when no selection or at selection boundary)
            if active and not has_selection() and ((os_clock() - st.blink_time) % 1.0 < 0.5) then
                local cw = get_text_width(string_sub(st.text, 1, st.cursor_pos), cfg.font_widget)
                draw_rect(tx + cw, vy + s_func(4), s_func(2), h - s_func(8), "Accent")
            end
        end
    end
    ui.cy = ui.cy + h + cfg.item_spacing
    return st.text, st.active
end

-- [ColorPicker - Inline Collapsible]

local cp_labels_cache = {}
local cp_expanded_state = {}

-- Convert RGB to Hex string
local function rgb_to_hex(r, g, b)
    return string_format("#%02X%02X%02X", math_floor(r), math_floor(g), math_floor(b))
end

function widgets.ColorPicker(label, tbl, key)
    if not tbl[key] then return end

    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local ui_input = ui.input
    local c = tbl[key]
    local w, h = layout.GetWidth(), cfg.item_h
    local vy = get_visual_y()
    local is_expanded = cp_expanded_state[key] or false

    -- Main row: label + hex + color preview
    if is_item_visible(vy, h) then
        local hover = is_mouse_in(ui.cx, vy, w, h)
        if ui.active_combo then hover = false end

        if hover and ui_input.mouse_left_just_pressed then
            cp_expanded_state[key] = not is_expanded
            is_expanded = cp_expanded_state[key]
            trigger_click_anim(ui.cx, vy, w, h)
        end

        draw_widget_background(ui.cx, vy, w, h, hover)

        -- Left: label
        local clean_label = get_clean_label(label)
        draw_text(clean_label, ui.cx + cfg.widget_text_padding, vy, cfg.font_widget, hover and "Text Primary" or "Text Secondary", nil, h)

        -- Right: hex value + color square
        local color_square_size = cfg.checkbox_height or 22  -- Same size as toggle switch
        local padding = cfg.widget_text_padding
        local square_x = ui.cx + w - color_square_size - padding
        local square_y = vy + (h - color_square_size) * 0.5

        draw_rect(square_x, square_y, color_square_size, color_square_size, c, cfg.checkbox_knob_rounding or 4)

        local hex_text
        local hc = cp_hex_cache[key]
        if hc and hc.r == c.r and hc.g == c.g and hc.b == c.b then
            hex_text = hc.hex
        else
            hex_text = rgb_to_hex(c.r, c.g, c.b)
            cp_hex_cache[key] = { r = c.r, g = c.g, b = c.b, hex = hex_text }
        end
        local hex_w = get_text_width(hex_text, cfg.font_small)
        local hex_x = square_x - hex_w - s_func(8)
        draw_text(hex_text, hex_x, vy, cfg.font_small, "Text Secondary", nil, h)
    end

    ui.cy = ui.cy + h + cfg.item_spacing

    -- Expanded: RGBA sliders
    if is_expanded then
        if not cp_labels_cache[key] then
            cp_labels_cache[key] = { r="R##"..key, g="G##"..key, b="B##"..key, a="A##"..key }
        end
        local l = cp_labels_cache[key]

        local r = widgets.Slider(l.r, c.r, 0, 255, 1)
        local g = widgets.Slider(l.g, c.g, 0, 255, 1)
        local b = widgets.Slider(l.b, c.b, 0, 255, 1)
        local a = widgets.Slider(l.a, c.a, 0, 255, 1)

        if r~=c.r or g~=c.g or b~=c.b or a~=c.a then
            c.r,c.g,c.b,c.a = r,g,b,a
            local cache = state.cache.color_userdata
            if cache[key] then
                cache[key] = color(r, g, b, a)
                state.cache.color_alpha[key] = {}
            end
        end
    end
end

function widgets.Selectable(label, selected, tags, hint)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local ui_input = ui.input
    local w, h = layout.GetWidth(), cfg.item_h
    local vy = get_visual_y()
    local clicked = false

    if is_item_visible(vy, h) then
        local hover = is_mouse_in(ui.cx, vy, w, h)
        if ui.active_combo or ui.active_id == "window_scrollbar" then hover = false end

        -- Update menu hint on hover
        if hover and hint then
            ui.menu_hint.current = hint
            ui.menu_hint.last_update = os_clock()
        end

        if hover and ui_input.mouse_left_just_pressed then
            clicked = true
            trigger_click_anim(ui.cx, vy, w, h)
        end

        local bg = selected and "Player Selected Background" or (hover and "Widget Hover" or "Widget Background")
        draw_rect(ui.cx, vy, w, h, bg, cfg.widget_rounding)

        local col = selected and "Accent" or (hover and "Text Primary" or "Text Secondary")
        draw_text(get_clean_label(label), ui.cx + cfg.widget_text_padding, vy, cfg.font_widget, col, nil, h)

        if tags then
            local tx = ui.cx + w - cfg.widget_text_padding
            local f_small = cfg.font_small
            local ts = s_func(4)
            for i = #tags, 1, -1 do
                local tag = tags[i]
                local tw = get_text_width(tag.text, f_small)
                tx = tx - tw - ts
                if tx > ui.cx + s_func(100) then
                    renderer.draw_text_ex(tag.text, tx, vy, f_small, tag.color or "Text Secondary", nil, h, 1.0, nil, "tag:" .. tag.text)
                end
            end
        end
    end
    ui.cy = ui.cy + h + cfg.item_spacing
    return clicked
end

-- [InlineTabBar]

function widgets.InlineTabBar(options, current_idx)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local ui_input = ui.input
    local h = cfg.inline_tab_height or s_func(24)
    local vy = get_visual_y()
    local font_size = cfg.font_small or 13
    local indicator_h = cfg.inline_tab_indicator_height or s_func(2)

    if is_item_visible(vy, h) then
        local gap = cfg.inline_tab_gap or s_func(4)

        -- Calculate text widths
        local text_widths = {}
        for i, name in ipairs(options) do
            text_widths[i] = get_text_width(name, font_size) + s_func(16)
        end

        -- Draw tabs
        local tx = ui.cx
        for i, name in ipairs(options) do
            local tab_w = text_widths[i]
            local active = (i == current_idx)
            local hover = is_mouse_in(tx, vy, tab_w, h)

            -- Text
            local col = active and "Inline Tab Active" or (hover and "Text Primary" or "Inline Tab Inactive")
            draw_text(name, tx, vy, font_size, col, tab_w, h - indicator_h)

            -- Indicator line
            if active then
                draw_rect(tx, vy + h - indicator_h, tab_w, indicator_h, "Inline Tab Indicator", s_func(1))
            end

            -- Click handling
            if hover and ui_input.mouse_left_just_pressed then
                current_idx = i
            end

            tx = tx + tab_w + gap
        end
    end

    ui.cy = ui.cy + h + (cfg.item_spacing or s_func(8))
    return current_idx
end

-- [Text]

function widgets.Text(text, color_key)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local h = cfg.item_h
    local vy = get_visual_y()

    if is_item_visible(vy, h) then
        draw_text(text, ui.cx + cfg.widget_text_padding, vy, cfg.font_widget, color_key or "Text Secondary", nil, h)
    end

    ui.cy = ui.cy + h + cfg.item_spacing
end

-- [Separator]

function widgets.Separator(label)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local w = layout.GetWidth()
    local line_h = cfg.separator_height or s_func(1)
    local margin = cfg.separator_margin or s_func(8)
    local font_size = cfg.font_small or 13
    local vy = get_visual_y()

    -- Total height includes text height for labeled separators
    local total_h = margin * 2 + (label and label ~= "" and font_size or line_h)
    local center_y = vy + total_h * 0.5
    local line_y = center_y - line_h * 0.5

    if is_item_visible(vy, total_h) then
        if label and label ~= "" then
            -- Separator with text
            local text_w = get_text_width(label, font_size)
            local text_padding = s_func(8)
            local text_x = ui.cx + (w - text_w) * 0.5

            -- Left line
            local left_w = text_x - text_padding - ui.cx
            if left_w > 0 then
                draw_rect(ui.cx, line_y, left_w, line_h, "Separator Line")
            end

            -- Text (centered vertically)
            draw_text(label, text_x, vy, font_size, "Separator Text", text_w, total_h)

            -- Right line
            local right_x = text_x + text_w + text_padding
            local right_w = ui.cx + w - right_x
            if right_w > 0 then
                draw_rect(right_x, line_y, right_w, line_h, "Separator Line")
            end
        else
            -- Simple line - centered vertically
            draw_rect(ui.cx, line_y, w, line_h, "Separator Line")
        end
    end

    ui.cy = ui.cy + total_h
end

-- [ProgressBar]

function widgets.ProgressBar(label, value, color_key)
    local ui = state.ui
    local cfg = state.deps.config_sys.data.current
    local w = layout.GetWidth()
    local h = cfg.progress_height or s_func(8)
    local item_h = cfg.item_h or s_func(40)
    local vy = get_visual_y()

    -- Clamp value
    value = math_max(0, math_min(1, value or 0))

    if is_item_visible(vy, item_h) then
        local padding = cfg.widget_text_padding or s_func(10)
        local bar_y = vy + (item_h - h) * 0.5
        local bar_rounding = cfg.progress_rounding or s_func(4)

        -- Background
        draw_rect(ui.cx, bar_y, w, h, "Progress Background", bar_rounding)

        -- Fill
        local fill_w = math_max(bar_rounding, w * value)
        draw_rect(ui.cx, bar_y, fill_w, h, color_key or "Progress Fill", bar_rounding)

        -- Label
        if label and label ~= "" then
            draw_text(label, ui.cx + padding, vy, cfg.font_small or 13, "Text Secondary", nil, item_h * 0.5)
        end

        -- Percentage (using pre-cached strings)
        local pct_int = math_floor(value * 100)
        local pct_str = progress_pct_cache[pct_int] or (tostring(pct_int) .. "%")
        local pct_w = get_text_width(pct_str, cfg.font_small or 13)
        draw_text(pct_str, ui.cx + w - pct_w - padding, vy, cfg.font_small or 13, "Text Primary", nil, item_h * 0.5)
    end

    ui.cy = ui.cy + item_h + (cfg.item_spacing or s_func(8))
    return value
end

function widgets.clear_cache()
    slider_value_cache = {}
    cp_labels_cache = {}
    cp_hex_cache = {}
end

return widgets
